/******************************************************************************************************
Copyright (C) 2011 Mestrelab Research S.L. All rights reserved.

This file is part of the MNova scripting toolkit.

Authorized users of MNova Software may use this file freely, but this file is provided AS IS
with NO WARRANTY OF ANY KIND, INCLUDING THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE.
*****************************************************************************************************/

/*globals Application, MessageBox, qsTr, settings, Dir, CommonSlots, ProgressDialog, serialization, MassItem, Str, MnUi, File, TextStream, date2ISOString*/
/*jslint plusplus: true, indent: 4*/

function UVSubtractor(aBackFiles, aForeFiles, aSingleMode, aPrecision, aResDir, aPeakSubtraction) {
	"use strict";
	this.aBackFiles = aBackFiles;
	this.aForeFiles = aForeFiles;
	this.aSingleMode = aSingleMode;
	this.aPrecision = aPrecision;
	this.aPeakSubtraction = aPeakSubtraction;
	this.aResDir = aResDir;
	this.methodsTable = [];
	this.impuritiesTable = [];
}

function UVSubtraction() {
	"use strict";
	//Version check
	if (Application.version.revision < 9556) {
		MessageBox.critical(qsTr("You are running {0} {1}.\nThis script function can be run with MNova version 7.1.1 and higher.").format(Application.name, Application.version.full));
		return;
	}

	var backFiles, foreFiles, precision, backDirName, backDir, foreDirName, foreDir, resDirName, resDir,
		peakSubtraction, singleBackSpec, ultravioletSubtractor,
		showDialog = true,
		msgFolderNotExists = qsTr("Folder {0} does not exist."),
	//Registry values
		BackFolderKey = "UV Subtraction/Backfolder Directory",
		ForeFolderKey = "UV Subtraction/Forefolder Directory",
		ResFolderKey = "UV Subtraction/Results Directory",
		BackMaskKey = "UV Subtraction/Backfolder Mask",
		ForeMaskKey = "UV Subtraction/Forefolder Mask",
		SingleBackKey = "UV Subtraction/Single background Spectrum",
		PeakSubtractionKey = "UV Subtraction/Peak Subtraction",
		PrecisionKey = "UV Subtraction/Arithmetic Precision",
	//Dialogs
		diag = Application.loadUiFile("ricares:UVSubtraction.ui");

	//-----------------------HANDLERS SECTION-------------------------
	function btBackMSHandler() {
		diag.widgets.btBackMS.clicked.connect(diag.widgets.leBackFolder, CommonSlots.onOpenDir);
	}

	function btForeMSHandler() {
		diag.widgets.btForeMS.clicked.connect(diag.widgets.leForeFolder, CommonSlots.onOpenDir);
	}

	function btResHandler() {
		diag.widgets.btResults.clicked.connect(diag.widgets.leResFolder, CommonSlots.onOpenDir);
	}

	//Initial values
	diag.widgets.leBackFolder.text = settings.value(BackFolderKey, Dir.home());
	diag.widgets.leForeFolder.text = settings.value(ForeFolderKey, Dir.home());
	diag.widgets.leResFolder.text = settings.value(ResFolderKey, Dir.home());
	diag.widgets.leBackMask.text = settings.value(BackMaskKey, "");
	diag.widgets.leForeMask.text = settings.value(ForeMaskKey, "");
	diag.widgets.cbSingleBackSpec.checked = settings.value(SingleBackKey, false);
	diag.widgets.cbPeakSubtraction.checked = settings.value(PeakSubtractionKey, false);
	diag.widgets.sbPrecision.value = settings.value(PrecisionKey, 4);

	//Handlers
	btBackMSHandler();
	btForeMSHandler();
	btResHandler();

	//---------------------Load Settings--------------------------
	while (showDialog) {
		if (!diag.exec()) {
			return;
		}
		precision = diag.widgets.sbPrecision.value;
		backDirName = diag.widgets.leBackFolder.text;
		backDir = new Dir(backDirName);
		if (!backDirName || !backDir.exists) {
			MessageBox.critical(msgFolderNotExists.format(backDirName));
			diag.widgets.leBackFolder.setFocus();
		} else {
			foreDirName = diag.widgets.leForeFolder.text;
			foreDir = new Dir(foreDirName);
			if (!backDirName || !foreDir.exists) {
				MessageBox.critical(msgFolderNotExists.format(foreDirName));
				diag.widgets.leForeFolder.setFocus();
			} else {
				resDirName = diag.widgets.leResFolder.text;
				resDir = new Dir(resDirName);
				if (!resDirName || !resDir.exists) {
					MessageBox.critical(msgFolderNotExists.format(resDirName));
					diag.widgets.leResFolder.setFocus();
				} else {
					if (diag.widgets.leBackMask.text.length) {
						backFiles = backDir.entryListAdv(diag.widgets.leBackMask.text);
					} else {
						backFiles  = undefined;
					}
					if (diag.widgets.leForeMask.text.length) {
						foreFiles = foreDir.entryListAdv(diag.widgets.leForeMask.text);
					} else {
						foreFiles  = undefined;
					}

					if (diag.widgets.cbSingleBackSpec.checked && backFiles.length > 1) {
						MessageBox.critical(qsTr("Found more than one background spectrum."));
						diag.widgets.leBackMask.setFocus();
					} else {
						if (!foreFiles || !backFiles || (!diag.widgets.cbSingleBackSpec.checked && backFiles.length !== foreFiles.length)) {
							MessageBox.critical(qsTr("The number of  background and foreground spectra must be the same."));
							diag.widgets.leBackMask.setFocus();
						} else {
							if (backFiles.length === 0) {
								MessageBox.critical(qsTr("No background spectrum found."));
								diag.widgets.leBackMask.setFocus();
							} else {
								if (foreFiles.length === 0) {
									MessageBox.critical(qsTr("No foreground spectrum found."));
									diag.widgets.leForeMask.setFocus();
								} else {
									showDialog = false;
								}
							}
						}
					}
				}
			}
		}
	}

	peakSubtraction = diag.widgets.cbPeakSubtraction.checked;
	singleBackSpec = diag.widgets.cbSingleBackSpec.checked;

	//Save values to registry
	settings.setValue(BackFolderKey, backDirName);
	settings.setValue(ForeFolderKey, foreDirName);
	settings.setValue(ResFolderKey, resDirName);
	settings.setValue(BackMaskKey, diag.widgets.leBackMask.text);
	settings.setValue(ForeMaskKey, diag.widgets.leForeMask.text);
	settings.setValue(SingleBackKey, singleBackSpec);
	settings.setValue(PeakSubtractionKey, peakSubtraction);
	settings.setValue(PrecisionKey, precision);

	ultravioletSubtractor = new UVSubtractor(backFiles, foreFiles, singleBackSpec, precision,  resDirName, peakSubtraction);
	ultravioletSubtractor.processAllSpectra();
}

UVSubtractor.prototype.processAllSpectra = function () {
	"use strict";

	var i, method, vector, backPage, dw, bkgMSItem, frgMSItem, result, parm, dir, existsChromatogram, tic, peaks, aux, max, j, completeDate, fileName, s, f,
		errors = "",
		aBackFiles = this.aBackFiles,
		aForeFiles = this.aForeFiles,
		aSingleMode = this.aSingleMode,
		aPeakSubtraction = this.aPeakSubtraction,
		methodsTable = this.methodsTable,
		startDate = date2ISOString(new Date()),
		startDateDir = startDate.replace(new RegExp("\\:", 'g'), "."),
		tempDir = new Dir(this.aResDir),
		aResDir,
		progress = new ProgressDialog();

	tempDir.mkdir(startDateDir);
	tempDir.cd(startDateDir);
	aResDir = tempDir.absPath;
	progress.minimumDuration = 0;
	progress.setRange(0, aForeFiles.length);
	progress.show();

	try {
		for (i = 0; i < aForeFiles.length && !progress.wasCanceled; i++) {
			progress.value = i;
			progress.labelText = qsTr("Subtracting {0} of {1}...").format(i + 1, aForeFiles.length);
			method = {};
			vector = [];

			method.vector = vector;
			Application.mainWindow.newDocument();

			if (aSingleMode) {
				serialization.open(aBackFiles[0]);
			} else {
				serialization.open(aBackFiles[i]);
			}
			backPage = Application.mainWindow.activeDocument.curPage();
			serialization.open(aForeFiles[i]);
			dw = Application.mainWindow.activeDocument;

			bkgMSItem = new MassItem(dw.item(0, "Mass Spectrum"));

			if (!bkgMSItem.isValid()) {
				errors += qsTr("Two mass items: background and foreground are needed. File: {0}\n").format(aForeFiles[i]);
				continue;
			}
			frgMSItem = new MassItem(dw.item(1, "Mass Spectrum"));

			if (!frgMSItem.isValid()) {
				errors += qsTr("Two mass items: background and foreground are needed. File: {0}\n").format(aForeFiles[i]);
				continue;
			}

			frgMSItem.deletePlot(1);
			parm = {};
			parm.injectionIndex = 0;
			parm.traceIndex = "DAD";
			parm.type = "TABS";
			existsChromatogram = frgMSItem.newChromatogram(parm);

			if (!existsChromatogram) {
				errors += qsTr("Unable to open UV chromatogram. File: {0}\n").format(aForeFiles[i]);
				return;
			}

			frgMSItem.deletePlot(0);
			result = frgMSItem.applyUVBackgroundSubtraction(bkgMSItem, 0, 0);

			if (!result) {
				errors += qsTr("Error applying UV subtraction. File: {0}\n").format(aForeFiles[i]);
				Application.mainWindow.activeDocument.close();
			}

			Application.mainWindow.activeDocument.deletePages(backPage);
			frgMSItem.update();

			if (aPeakSubtraction) {
				frgMSItem.selectionMode = MassItem.SpectrumSelMode.PeakSub;
			} else {
				frgMSItem.selectionMode = MassItem.SpectrumSelMode.Peak;
			}

			tic = frgMSItem.plot();
			peaks = tic.peaks;
			if (!tic.peaks) {
				errors += qsTr("No peaks found in TIC : {0}\n").format(frgMSItem.datasetFileName);
				continue;
			}
			aux = this;
			max = 0;
			method.totalArea = 0;

			frgMSItem.selectSpectra(tic, peaks[0].center.x);

			if (frgMSItem.plotsCount < 2) {
				errors += qsTr("Error applying UV subtraction. File: {0}\n").format(aForeFiles[i]);
				Application.mainWindow.activeDocument.close();
			} else {
				for (j = 0; j < peaks.length && !progress.wasCanceled; j++) {
					if (peaks[j].area > peaks[max].area) {
						max = j;
					}
					method.totalArea += peaks[j].area; //Calculate Total Area
					method.vector.push(this.getPeakValues(frgMSItem, peaks[j]));
				}
				fileName = aResDir + "/Method" +  String.fromCharCode(65 + i) + ".mnova";
				serialization.save(fileName, "mnova"); // save MNova file format
				Application.mainWindow.activeDocument.close();

				if (!progress.wasCanceled) {
					method.vector[max].type = "Parent";
					methodsTable.push(method);
				}
			}
		}
		if (!progress.wasCanceled) {
			Application.mainWindow.newDocument();
			this.createReports();
		}
	} finally {
		progress.close();
		if (errors !== "") {

			completeDate = date2ISOString(new Date());
			completeDate = completeDate.replace(new RegExp("\\:", 'g'), "_");
			dir = new Dir(aResDir);
			f = new File(dir.filePath(completeDate + "_ErrorsLog.txt"));
			f.open(File.WriteOnly);
			s = new TextStream(f);
			s.write(errors);
			s.flush();
			f.close();
		}
	}
};

UVSubtractor.prototype.getPeakValues = function (massItem, peak) {
	"use strict";
	var peaks, max = 0, i, ion,
		peakValues = {};

	massItem.selectSpectra(massItem.plot(), peak.center.x);
	massItem.update();
	peakValues.RT = peak.center.x;
	peakValues.area = peak.area;
	peaks = massItem.plot(1).peaks;
	for (i = 0; i < peaks.length; i++) {
		if (peaks[i].center.y > peaks[max].center.y) {
			max = i;
		}
	}
	ion = peaks[max].center.x;
	peakValues.ion = ion.toFixed(0);
	peakValues.type = "Impurity";
	return peakValues;
};

UVSubtractor.prototype.createReports = function () {
	"use strict";
	var i, parentIon, title, header, table, row, j, impurity, relativeArea, item, parentAverage,
		precision = this.aPrecision,
		methodsTable = this.methodsTable,
		impuritiesTable = this.impuritiesTable,
		drawnItems = [],
		parentAverageSum = 0;

	for (i = 0; i < methodsTable.length; i++) {
		title = [];
		header = [];
		table = [];

		title.push("Method " +  String.fromCharCode(65 + i));
		header.push("Peak   ");
		header.push("Retention Time ");
		header.push("Area Percent   ");
		header.push("Key Ion");

		table.push(title);
		table.push(header);

		parentIon = "";

		//Loop to draw method tables

		for (j = 0; j < methodsTable[i].vector.length; j++) {
			//Create a j row for i method table
			row = [];
			row.push(this.getPeakName(methodsTable[i].vector[j], i));
			row.push(methodsTable[i].vector[j].RT.toFixed(precision));
			row.push((methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea).toFixed(precision));
			row.push(methodsTable[i].vector[j].ion);
			table.push(row);

			//Calculate sum of areas for every method

			if (methodsTable[i].vector[j].type === "Parent") {
				//Treatment for the parent compound
				parentAverageSum += (methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea);
				parentIon = methodsTable[i].vector[j].ion;
			} else {
				//Treatment in case of impurity
				impurity = this.getPeakName(methodsTable[i].vector[j], i).substring(this.getPeakName(methodsTable[i].vector[j], i).indexOf(" ")) - 1;
				relativeArea = methodsTable[i].vector[j].area * 100 / methodsTable[i].totalArea;

				if ((relativeArea) > impuritiesTable[impurity].area) {
					//New cluster representant impurity found
					impuritiesTable[impurity].area = relativeArea;
					impuritiesTable[impurity].method = i;
				}
			}
		}
		item = Application.draw.text(Str.rowsToHtmlTable(table, header.length), true);
		drawnItems.push(item);
	}

	//Final Purity table: Table with all detected impurities included at their highest peak areas and the arithmetic mean of the percent area of the parent compound.

	title = [];
	header = [];
	table = [];

	title.push("Final Purity Table");
	header.push("Peak	");
	header.push("Area Percent	");
	header.push("Key Ion");

	table.push(title);
	table.push(header);

	//Create impurity rows
	for (i = 0; i < impuritiesTable.length; i++) {
		row = [];
		row.push(impuritiesTable[i].name);
		row.push(impuritiesTable[i].area.toFixed(precision));
		row.push(impuritiesTable[i].ion);
		table.push(row);
	}

	//Create parent compound row
	row = [];
	row.push("Parent");
	parentAverage = parentAverageSum / methodsTable.length;
	row.push(parentAverage.toFixed(precision));
	row.push(parentIon);
	table.push(row);

	item = Application.draw.text(Str.rowsToHtmlTable(table, header.length), true);
	drawnItems.push(item);

	for (i = 1; i < drawnItems.length; i++) {
		drawnItems[i].translate(drawnItems[i - 1].right - drawnItems[i].left, 0);
		drawnItems[i].update();
	}
	Application.mainWindow.activeDocument.update();
};

UVSubtractor.prototype.getPeakName = function (peak, methodNumber) {
	"use strict";
	var found, i, maxPeak, node,
		impuritiesTable = this.impuritiesTable;

	if (peak.type === "Parent") {
		return peak.type;
	}

	found = false;
	i = 0;
	maxPeak = 0;
	while (i < impuritiesTable.length && !found) {
		if (peak.ion === impuritiesTable[i].ion) {
			found = true;
		} else {
			i++;
		}
	}

	if (found) {
		return impuritiesTable[i].name;
	}

	//New impurity found. Add to impurities table and return value.
	node = {};
	node.ion = peak.ion;
	node.name = "Impurity " + (impuritiesTable.length + 1);
	node.area = 0;
	node.method = methodNumber;
	impuritiesTable.push(node);
	return node.name;

};

if (this.MnUi && MnUi.scripts_mass) {
	MnUi.scripts_mass.scripts_mass_UVSubtraction = UVSubtraction;
}
